Alexa 开发新技能 - Lambda

非常简单的教程,讲怎么给 Alexa 添加新的 skill,让你的 Echo 更个性化。本篇添加的 skill 是让 Alexa 从 reddit 上读前 10 条热点。

代码戳Alexa-Starter-RedditReader,其他版本如用 python flask 实现,见Alexa 开发新技能 - python flask。这一篇截图比较细,一方面是因为 AWS 版本迭代太快,网上之前的教程可能会过时,另一方面实在是因为一步步做下来踩了很多坑,争取这篇教程可以让大家少走一些弯路。

这一篇会用到 AWS Lambda,Lambda 的优势官方说明描述的很清楚,简单来说最显著的优点就是,与 Alexa 开发新技能 - python flask 相比,我们不再需要后端运行代码并通过 ngrok 等工具将代码部署到公开网络。

通过 AWS Lambda,无需配置或管理服务器即可运行代码。您只需按消耗的计算时间付费 – 代码未运行时不产生费用。借助 Lambda,您几乎可以为任何类型的应用程序或后端服务运行代码,而且全部无需管理。只需上传您的代码,Lambda 会处理运行和扩展高可用性代码所需的一切工作。您可以将您的代码设置为自动从其他 AWS 服务触发,或者直接从任何 Web 或移动应用程序调用。

总结下来 AWS Lambda 有以下几个特点:

  • run code in response to events
  • no maintenance of server, no worry about infrastructure
  • scale automatically
  • never pay for idle

下面介绍怎么来用 Nodejs 和 Lambda 实现 Reddit Reader。

Requirements

Code for new skill (Lambda Function)

这里我们使用 Lambda function,get_headlines() 是我们主要的 method,从 Reddit 里返回 10 条热点。逻辑是这样的:

  • (用户呼唤 Reddit Reader)
  • Alexa 问用户 ‘Hello there, would you like the news?’
  • 用户回答
    肯定回复: 读 10 headlines
    否定回复: 回答 ‘I am not sure why you asked me to run then, but okay… bye’

首先建好 project 框架

1
2
3
4
5
$ mkdir alexaproject
$ cd alexaproject/
$ npm init
$ mkdir src
$ touch src/index.js

设置相关依赖

1
2
$ npm install alexa-sdk --save
$ npm install request --save

版本:

  • “alexa-sdk”: “^1.0.9”,
  • “request”: “^2.81.0”

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
'use strict';
var Alexa = require("alexa-sdk");
var request = require('request');
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context, callback);
alexa.appId = "amzn1.ask.skill.[YOUR_APP_ID]";
alexa.registerHandlers(handlers);
alexa.execute();
}
var handlers = {
"LaunchRequest": function() {
var speechOutput = "Hello there, would you like the news?";
var reprompt = speechOutput;
this.emit(':ask', speechOutput, reprompt);
},
"YesIntent": function() {
var self = this;
get_headlines(function(headlines) {
var speechOutput = 'The current world news headlines are ' + headlines;
self.emit(':tell', speechOutput);
});
},
"NoIntent": function() {
var speechOutput = 'I am not sure why you asked me to run then, but okay... bye'
this.emit(':tell', speechOutput);
},
"AMAZON.StopIntent": function() {
var speechOutput = "Good bye! Thank you for using Reddit Reader";
this.emit(':tell', speechOutput);
},
"AMAZON.CancelIntent": function() {
var speechOutput = "Good bye! Thank you for using Reddit Reader";
this.emit(':tell', speechOutput);
},
}
function get_headlines(callback) {
request('https://reddit.com/r/worldnews/.json?limit=10', function (error, response, body) {
if (!error && response.statusCode == 200) {
var body=JSON.parse(body)['data']['children'];
var res = "";
body.forEach(function(ele){
res = res + ele['data']['title'] + " ";
});
}
return callback(res);
});
}

如果不用 alexa-sdk,也可以自己搭一个框架出来,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
'use strict';
var request = require('request');
exports.handler = function(event, context) {
try {
console.log("event.session.application.applicationId") + event.session.application.applicationId;
/**
* Uncomment this if statement and populate with your skill's application ID to
* prevent someone else from configuring a skill that sends requests to this function.
*/
if (event.session.application.applicationId !== "[YOUR_APP_ID]") {
context.fail("Invalid Application ID");
}
if (event.session.new) {
onSessionStarted({requestId: event.request.requestId}, event.session);
}
if (event.request.type === "LaunchRequest") {
onLaunch(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "IntentRequest") {
onIntent(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "SessionEndedRequest") {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
context.fail("Exception: " + e);
}
}
/**
* called when the user invokes the skill without specifying what they want.
*/
function onLaunch(launchRequest, session, callback) {
getWelcomeResponse(callback)
}
/**
* Called when the user specifies an intent for this skill.
*/
function onIntent(intentRequest, session, callback) {
var intent = intentRequest.intent
var intentName = intentRequest.intent.name;
// dispatch custom intents to handlers here
if (intentName == "") {
handleResponse(intent, session, callback)
} else if (intentName == "YesIntent") {
handleYesResponse(intent, session, callback)
} else if (intentName == "NoIntent") {
handleNoResponse(intent, session, callback)
} else if (intentName == "AMAZON.StopIntent") {
// not used here
// handleGetHelpRequest(intent, session, callback)
} else if (intentName == "AMAZON.CancelIntent") {
handleFinishSessionRequest(intent, session, callback)
} else if (intentName == "AMAZON.HelpIntent") {
handleFinishSessionRequest(intent, session, callback)
} else {
throw "Invalid intent"
}
}
/**
* Called when the session starts.
*/
function onSessionStarted(sessionStartedRequest, session) {
console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId
+ ", sessionId=" + session.sessionId);
}
/**
* Called when the user ends the session.
* Is not called when the skill returns shouldEndSession=true.
*/
function onSessionEnded(sessionEndedRequest, session) {
console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId
+ ", sessionId=" + session.sessionId);
// Add cleanup logic here
}
function getWelcomeResponse(callback) {
var speechOutput = "Hello there, would you like the news?"
var reprompt = speechOutput
var header = "news"
var shouldEndSession = false
var sessionAttributes = {
"speechOutput": speechOutput,
"repromptText": reprompt
}
callback(sessionAttributes, buildSpeechletResponse(header, speechOutput, reprompt, shouldEndSession))
}
function handleYesResponse(intent, session, callback) {
get_headlines(function(headlines) {
var speechOutput = 'The current world news headlines are ' + headlines;
var shouldEndSession = true
callback(session.attributes, buildSpeechletResponseWithoutCard(speechOutput, "", shouldEndSession))
});
}
function handleNoResponse(intent, session, callback) {
var speechOutput = 'I am not sure why you asked me to run then, but okay... bye'
var shouldEndSession = true
callback(session.attributes, buildSpeechletResponseWithoutCard(speechOutput, "", shouldEndSession))
}
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
card: {
type: "Simple",
title: title,
content: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function buildSpeechletResponseWithoutCard(output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: "1.0",
sessionAttributes: sessionAttributes,
response: speechletResponse
}
}
function handleFinishSessionRequest(intent, session, callback) {
callback(session.attributes, buildSpeechletResponseWithoutCard("Good bye! Thank you for using Reddit Reader","",true))
}
function get_headlines(callback) {
request('https://reddit.com/r/worldnews/.json?limit=10', function (error, response, body) {
if (!error && response.statusCode == 200) {
var body=JSON.parse(body)['data']['children'];
var res = "";
body.forEach(function(ele){
res = res + ele['data']['title'] + " ";
});
}
return callback(res);
});
}

更多方法戳alexa-skills-kit-sdk-for-nodejs

Lambda Configuration

登录AWS console在 Service 下选择 Lambda
LAMBDA1.png

Create a Lambda function,选 Node.js.4.3, Blank Function
LAMBDA2.png

Configure Triggers,选 Alexa Skills Kit
LAMBDA3.png

Configure Function,如下设置
LAMBDA4.png
LAMBDA5.png

注意两个点,代码上传后,Handler 的路径设置必须和代码中 handler 文件的路径相一致,建议对文件内所有文件打包,而不是直接对文件夹打包。Role 如果没有 existing role,可以新建一个。

完成后选择新建的 function,在 Action 里选择 Configure test event
LAMBDA6.png

选择 Alexa Start Session,修改 Applicatio ID,即 Alexa console 里的 Application ID,在 Deploy 下会讲到。
DEVELOPERCONSOLE.png
LAMBDA7.png

选择 Save and Test,返回结果
LAMBDA8.png

Deploy

Amazon developer 网站上注册用户并登陆,注意这里的用户名和你的 Echo 用户名是一致的。点开 Alexa tab,选择 add skill,开始部署。

Step1:
填写 Name 和 Invocation Name,Invocation Name 用来 invoke app,Application ID 可以在保存页面后找到,需要添加到代码以及 Configure test event 里,如果需要对 Lambda 页面进行测试的话。
1.jpg

Step2:
主要填写 Intent Schema 和 Sample Utterances

Intent Schema: how alexa will traverse your application

1
2
3
{ "intents": [{ "intent": "YesIntent" },
{ "intent": "NoIntent" }]
}

Sample Utterances: the words people say to trigger intent

1
2
3
4
5
YesIntent yes
YesIntent sure
NoIntent no
NoIntent go away

2.jpg 3.jpg

Step3:
选择并填写 Endpoint,即 AWS Lambda 下 function 的 ARN
6.png
4.png

Step4:
就可以用 Echo 来 test 啦~ 如果没有 Echo,可以用 Service Simulator 来模拟,可以输入 text,也可以输入 json
先给 Alexa 一个关于 Reddit Reader 的指令,然后按照我们的代码,Alexa 会问你要不要读新闻
7.jpg

然后我们回答 yes,Alexa 就开始读新闻啦~
8.jpg

徐阿衡 wechat
欢迎关注:徐阿衡的微信公众号
客官,打个赏呗~